#coding=utf-8
"""
LoNote intermediation convert 中介格式轉換模組

負責將其他資料格式，轉換為 LoNote 的標準資料格式 - IM。
目前，主要也就是將舊版的 LoNote 格式轉換為新版

=================================

參數命名法表記如下：

    src = 轉換的來源路徑
    dst = 轉換的目標路徑

"""

import os
import os.path
import shutil
import sqlite3
import datetime as DT
import zlib
import itertools
import re
import xml.etree.ElementTree as ET

import diff_match_patch

import civa.parse
from lonote.im import IM, IMAlreadyExist

class IMCVError(Exception): pass

def from_18X_getdst_fpath(dst, delete_target = False):
    '''檢查並修改路徑變數

        delete_target = 為 True 時表示目標存在時強制刪除原檔案
        '''
    # 確定 dst 是用 .lobook 結尾
    if dst.endswith(IM.ext) == False:
        dst = dst + IM.ext
    # 確定目標資料不存在
    if os.path.exists(dst):
        if delete_target:
            # 連資料夾一起刪除
            dst_dir = dst.rstrip(IM.ext)
            if os.path.isdir(dst_dir):
                shutil.rmtree(dst_dir)
            os.remove(dst)
        else:
            info = '"{}" already exist!'.format(dst)
            raise IMCVError(info)
    return dst

def from_18X(src, dst, *, flush = True, keep_history = True):
    '''將 lonote 1.8.X 系列最後版本的資料庫轉為新版 LoNote 的標準中介格式

        不會變更原始資料夾的內容。

        注意：
            1. 轉換時，會拋棄所有 viewer 側的資料，包括 bookmark、scrollpos
               、哪些筆記項目正處於 expand 狀態等。
               （但不會拋棄手動排序順序，排序本身算是筆記本的內容之一）
            2. 轉換時，歷史記錄時間如果遇到碰撞（通常是在1秒內連續記錄了兩
               筆，這種事非常罕見），則隨機放棄其中一筆。
            3. embedded 與 attachment 現在開始使用相同的命名空間。轉換時要是
               發生重名狀況，其中之一會被隨機忽略。
            4. 會自動將lowiki連結替換成標準連結。
            5. 不要吐嘈函式名！

        src = 來源（資料夾），為一個 lonote 1.X 系列最後版本的筆記目錄
        dst = 目標（檔案），為中介資料檔名。
        flush = 是否壓制成單一檔案。
                單一檔案比較整齊，但存取速度也比較慢。
        keep_history = 是否保存歷史記錄。因為過去實作方式的原因，
                       以前的歷史記錄項目很多同時還很難用。

        return ==> 成功或失敗
        '''
    def check_db_version_is_5(dbpath):
        '''確認當前資料庫版本正確。'''
        if not os.path.isfile(dbpath):
            raise IMCVError('"notebook.db" not found!')
        conn = sqlite3.connect(dbpath)
        conn.row_factory = sqlite3.Row
        db_version = conn.execute('pragma user_version').fetchone()[0]
        conn.close()
        if db_version != 5:
            raise IMCVError('Old LoNote db version Not Match.')

    def pid2notes_dict(dbpath):
        '''從資料庫中產生 pid -> [相同pid的notes(已用ord排序)] 字典

            dbpath = sqlite3資料庫檔案路徑
            '''
        conn = sqlite3.connect(dbpath)
        conn.row_factory = sqlite3.Row
        allnotes = conn.execute('select * from notes').fetchall()
        conn.close()
        # 替 allnotes 排序，才能正確 groupby
        def sortkey(n):
            if n['parentid'] == None:
                return 0
            else:
                return n['parentid']
        allnotes = sorted(allnotes, key = sortkey)
        # 將資料整理起來
        pid2notes = {}
        for pid, group_notes in itertools.groupby(
                allnotes, lambda x: x['parentid']):
            group_notes = sorted(group_notes, key = lambda x: int(x['ord']))
            pid2notes[pid] = group_notes
        return pid2notes

    def id2historys_dict(dbpath):
        '''從資料庫中產生 id -> [historys] 字典

            dbpath = sqlite3資料庫檔案路徑
            '''
        conn = sqlite3.connect(dbpath)
        conn.row_factory = sqlite3.Row
        allhistorys = conn.execute('select * from history').fetchall()
        allhistorys = sorted(allhistorys, key = lambda x: x['id'])
        conn.close()
        id2historys = {} # 每一個 id 都可能對應有多筆歷史記錄
        for id, group_historys in itertools.groupby(
                allhistorys, lambda x: x['id']):
            id2historys[id] = sorted(
                    group_historys, key = lambda x: x['modtime'])
        return id2historys

    def write_in_im(data_dir, pid2notes, id2historys):
        '''將pid2notes寫入im中'''
        def history_decode(history):
            content = history['content']
            deltafrom = history['deltafrom']
            # content解壓縮
            content = zlib.decompress(content).decode('utf-8')
            # 測試是否需要復原delta
            if deltafrom is None: #如果沒有delta訊息就不用做任何事
                pass
            else: #否則就將他恢復
                deltaFrom_history = next(
                        h for h in id2historys[history['id']]
                        if h['modtime'] == deltafrom)
                deltaFromContent = history_decode(deltaFrom_history)
                dmp = diff_match_patch.diff_match_patch()
                content,is_OK = dmp.patch_apply(dmp.patch_fromText(content), deltaFromContent)
            return content

        def write(note, pnote = None):
            '''寫入該 note 的所有相關內容'''
            def modify(content):
                '''微調新版與舊版之間content的差異'''
                #只留下body部份
                new_content1 = civa.parse.html2content(content)
                #將lowiki替換成標準連結
                ans = re.sub(r'<a href="lowiki:(.*?)">',
                        r'<a href="../\1/">',
                        new_content1)
                return ans

            print('======================================')
            try:
                print('Note:', note['name'])
            except UnicodeEncodeError as e:
                print('## Name cannot be displayed ##', '\n    >>', e)
            # 寫入 note 基本資料
            if pnote != None:
                im.note_create(title = note['name'],
                        parent_uuid = pnote['uuid'],
                        order = None,
                        new_uuid = note['uuid'])
            else: # 因為根已經自動建立好了，故不創建僅進行必要更新
                im.title_update(note['uuid'], note['name'])
            # 更新內容與日期
            raw_content = modify(note['content'])
            im.content_update(uuid = note['uuid'], content = raw_content)
            modtime = DT.datetime.fromtimestamp(float(note['modtime']))
            cretime = DT.datetime.fromtimestamp(float(note['cretime']))
            im.note_time_update(uuid = note['uuid'],
                    mtime = modtime, ctime = cretime)
            # 寫入 history record
            if note['id'] in id2historys and keep_history == True:
                print('History:')
                for history in id2historys[note['id']]:
                    hist_content = history_decode(history)
                    raw_hist_content = modify(hist_content)
                    frozen_time = DT.datetime.fromtimestamp(
                            float(history['modtime']))
                    print('     ', frozen_time.strftime(IM.time_format))
                    try:
                        im.record_create(uuid = note['uuid'],
                                name = None,
                                comment = 'Lost history',
                                content = raw_hist_content,
                                ftime = frozen_time)
                    except IMAlreadyExist:
                        pass # 有可能存在同樣 modtime 的資料，放棄這筆資料
            # 寫入 note 的附件與 embedded
            note_dir = os.path.join(data_dir, str(note['id']))
            for dirpath, dirnames, filenames in os.walk(note_dir):
                reldir = os.path.relpath(dirpath, start = note_dir)
                if reldir == 'embedded':
                    print('embedded:')
                    for filename in filenames:
                        try:
                            print('     ', filename)
                        except UnicodeEncodeError as e:
                            print('## Name cannot be displayed ##',
                                    '\n        >>', e)

                        filepath = os.path.join(dirpath, filename)
                        with open(filepath, mode = 'rb') as f:
                            data = f.read()
                        try:
                            im.resource_update(uuid = note['uuid'],
                                    name = filename,
                                    res_type = 'emb',
                                    data = data)
                        except IMAlreadyExist: pass
                elif reldir == 'attached':
                    print('attachment:')
                    for filename in filenames:
                        try:
                            print('     ', filename)
                        except UnicodeEncodeError as e:
                            print('## Name cannot be displayed ##',
                                    '\n        >>', e)

                        filepath = os.path.join(dirpath, filename)
                        with open(filepath, mode = 'rb') as f:
                            data = f.read()
                        try:
                            im.resource_update(uuid = note['uuid'],
                                    name = filename,
                                    res_type = 'att',
                                    data = data)
                        except IMAlreadyExist: pass
            print('') # 換行

        def walk_and_write(note, pnote = None):
            '''從當前的筆記往下走，邊走邊寫入 im 中'''
            write(note, pnote)
            if note['id'] in pid2notes:
                for subnote in pid2notes.get(note['id']):
                    walk_and_write(subnote, note)

        root = pid2notes[None][0] # 沒有pid的group 為 root group，只有一個
        print('Generate newfile...')
        im = IM(dst, bookuuid = root['uuid'])
        print('Write-in...')
        walk_and_write(root)
        return im

    print('Starting convert "{}"...'.format(src))
    print('Now checking ...')
    # ==========================
    # 早期變數定義
    # ==========================
    dst = from_18X_getdst_fpath(dst)
    dbpath = os.path.join(src, 'notebook.db')
    data_dir = os.path.join(src, 'data')
    # 檢查來源資料
    check_db_version_is_5(dbpath)
    # =======================
    # 一切早期檢查完成，正式開始處理啦！
    # =======================
    print('Pre-load old DB data...')
    pid2notes = pid2notes_dict(dbpath)
    id2historys = id2historys_dict(dbpath)
    im = write_in_im(data_dir, pid2notes, id2historys)
    if flush:
        print('Flush all data...\n')
        im.flush() # 壓製
    im.close()
    print('Convert Complete!\nyour new Notebook file now on "{}"'.format(dst))


# def to_website(src, dst, replace = True):
#     '''將一本 lobook 筆記簿，轉換成一個本地網站
# 
#         src = 完整的 lobook 來源檔案路徑
#         dst = 目標資料夾位置，
#         '''
#     # 確認路徑合法
#     if not os.path.isfile(src):
#         raise IMCVError('"{}" not a file!'.format(src))
#     if os.path.isdir(dst):
#         if replace:
#             shutil.rmtree(dst)
#         else:
#             raise IMCVError('"{}" are existed!'.format(dst))
# 
#     # 準備工作
#     im = IM(src)
#     xml = im.book_xml()
#     eroot = ET.fromstring(xml)
# 
#     page_template = '''<!DOCTYPE html>
# 
# <html lang="">
#     <head>
#         <meta charset="utf-8" />
# 
#         <title></title>
#         <meta name="description" content="{{book.title(current_uuid)}}" />
#         <meta name="uuid" content="{{current_uuid}}" />
#         <meta name="buuid" content="{{bookuuid}}" />
#     '''
# 
#     os.makedirs(dst)
# 
#     indexinfo = [] # 產生目錄用的資料
#     # 計算函式
#     def recu(enote):
#         '''遞迴產生每個頁面，並收集之後用來產生目錄的資料'''
#         # 工作
#         uuid = enote.get('uuid')
#         pagedir = os.path.join(dst, uuid)
#         os.mkdir(pagedir)
#         content = im.content(uuid)
#         title = im.title(uuid)
#         # 遞迴！
#         esubnotes = enote.findall('./note')
#         for e in esubnotes:
#             recu(e)
